Skip to content

feat(pkg-intel): add package_changelog (pkg changelog + MCP) - #18

Merged
jlitola merged 1 commit into
mainfrom
feat/pkg-intel-changelog
Apr 21, 2026
Merged

feat(pkg-intel): add package_changelog (pkg changelog + MCP)#18
jlitola merged 1 commit into
mainfrom
feat/pkg-intel-changelog

Conversation

@jlitola

@jlitola jlitola commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Fourth Wave 1 package-intelligence tool, following package_summary (#13), package_vulnerabilities (#14), and package_dependencies (#16 + follow-up #17). Behind the existing code_navigation capability gate.

  • githits pkg changelog [spec] — default latest-mode output is a summary row (identity · source · mode · count) plus, per entry, a version date url header followed by the first 10 lines of the markdown body, indented and dimmed. Bodies longer than the cap show a … (+N more lines — use --verbose for the full body) footer. --verbose uncaps the body preview; --no-body drops bodies entirely from both terminal and --json. --from <v> switches to range mode (all entries between --from and --to/latest; --limit rejected). --to <v> / --limit <n> shape latest mode. --git-ref targets a branch/tag for CHANGELOG.md source. --repo-url <url> is an alternative addressing mode, mutually exclusive with <spec>.
  • package_changelog — MCP tool with the same envelope. Dual addressing: registry + package_name XOR repo_url. Permissive Zod schema + in-handler validation. include_bodies (default true) mirrors CLI's --no-body and controls JSON envelope bodies.

Design choices

  • Dual addressing is unique to this tool. P1 / P2 / P3 all accept only registry + package_name. packageChangelog is intrinsically repo-level (sources: GitHub Releases, CHANGELOG.md, HexDocs), so repoUrl is a peer addressing mode in the GraphQL signature. Exposing it on MCP was non-negotiable: packageSummary cannot resolve repo-URL → spec, so agents starting from a repo URL had no path in. Documented in tools.md so future tool authors don't cargo-cult the asymmetry.
  • <spec>@<version> rejected. pkg vulns / pkg deps treat @version as "for this exact version"; changelog has no single-version query. Silently remapping to toVersion would be a client-invented semantic shift. Rejected with INVALID_ARGUMENT and a hint pointing to --to/--from.
  • Body preview capped at 10 lines by default. Release bodies routinely run 50-100+ lines — showing them unbounded would swamp the terminal; showing none (the earlier design) made the command useless for answering "what changed". The 10-line cap shows the first one or two sections plus preamble, which usually answers the question; --verbose lifts the cap when full context is needed; --no-body drops bodies entirely. JSON envelope is always full-body unless include_bodies: false--verbose is terminal-only.
  • Data-first envelope. {registry|repoUrl, source, mode, entries: {count, items}, filter?}. source always present (null-source case promoted to NOT_FOUND at the service boundary; envelope builder has a defence-in-depth throw if the invariant is ever violated upstream). mode derived from request — "range" iff fromVersion non-null, "latest" otherwise. entries.count computed client-side from items.length; backend count not selected on the wire.
  • version kept when null, other per-entry nullables stripped. version is the primary key agents index by. Empty-string values (e.g. body: "") are preserved — distinct from absent — so agents can tell "empty release notes" from "no notes field". Terminal formatter renders (empty release notes) sentinel for empty-string bodies under --verbose.
  • filter.* tracks explicit fields only. Builder returns an explicitFilterFields set; envelope emits filter.* only for caller-supplied inputs. Backend defaults (limit: 10, toVersion: latest) never round-trip as caller intent.
  • include_bodies lever. Default true. false drops body from every item explicitly; other fields preserved so agents still get the version / date / URL timeline. Measured 5.13× envelope size reduction on a 20-entry npm:typescript request (17.3 KB → 3.4 KB).
  • --no-body + --verbose is an error. Contradictory intents; CLI rejects with an actionable hint.
  • MCP limit validation in the shared builder, not Zod. If Zod hard-rejected limit: 51 at the SDK level, agents would see a raw SDK error instead of our shared {error, code, retryable} envelope. Schema is permissive; builder enforces bounds.
  • Registry coverage — all 9 on the wire, no client-side restriction. packageChangelog is source-pull, not registry-query. Live-smoke confirmed the backend returns useful data across npm / pypi / hex / crates / vcpkg / maven / packagist; nuget returns a generic BACKEND_ERROR; zig packages weren't indexed. All paths surface the shared envelope correctly.
  • Mode mutex enforced client-side. --from / from_version + --limit / limitINVALID_ARGUMENT with actionable hint.
  • metadata dropped in v1. Source-specific opaque GenericJSON; revisit via agent feedback.
  • Shared promoteGenericVersionNotFound extended. Now recognises fromVersion / toVersion in addition to version. registry / packageName made optional so repo-URL-addressed requests flow through. Preference order version → fromVersion → toVersion. P2 / P3 regressions guarded by existing tests + 5 new helper tests.

Tests

  • bun test — 1062 pass, 0 fail.
  • bun run typecheck / bun run build / bun run lint — clean.
  • Request builder: addressing XOR, <spec>@<version> rejection, --from/--limit mutex, limit bounds, tag-style rejection, pre-release versions on --from/--to, whitespace handling, explicit-flag tracking.
  • Envelope builder: shape, null handling, empty-string body preservation, include_bodies lever, mode derivation, filter echo, empty entries, defence-in-depth source-null throw.
  • Terminal formatter: default 10-line body cap + truncation footer, --verbose uncap, (empty release notes) sentinel, singular/plural footer wording, missing fields, range mode label.
  • MCP tool: metadata, happy path, repo-URL addressing, mode derivation, include_bodies, validation errors (incl. out-of-range limit via envelope not SDK), service errors.
  • CLI action: addressing, errors, --no-body+--verbose rejection, flag interactions, truncation footer on long bodies, --verbose uncap.
  • Parity test: 12 fixtures incl. happy latest, range, repo-URL, --no-body, default bodies, empty entries, NOT_FOUND, TargetNotFoundError, VERSION_NOT_FOUND, BACKEND_ERROR, INVALID_ARGUMENT.

Live-smoke (production pkgseer)

  • npm:express (latest / limit 3) — 3 entries, bodies rendered in-line with 10-line cap; under-cap bodies show in full with no footer; over-cap bodies show +62 more lines — use --verbose for the full body.
  • npm:express --from 5.0.0 --to 5.2.0 — range mode returns entries. Note: backend ordering isn't strictly newest-first in range mode (observed 5.2.0, 5.0.1, 5.1.0 — filed as backend gap feat(code-nav): ship capability-gated search_symbols (CLI + MCP) #10 for the pkgseer team).
  • --repo-url https://github.com/expressjs/express — repo-URL addressing works; envelope renders repo URL as identity.
  • npm:express@5.0.0 — rejected with hint pointing to --to/--from. Exit code 1.
  • npm:express --from v5.0.0 — tag-style rejection fires.
  • npm:express --from 5.0.0 --limit 5 — mode-mutex rejection fires.
  • npm:express --no-body --verbose — contradictory-flags rejection fires.
  • npm:obscure-nonexistent --json — backend's generic "Package not found" surfaces as BACKEND_ERROR (same pattern as P2/P3; backend doesn't emit structured PACKAGE_NOT_FOUND extension).
  • Registry matrix: npm ✓, pypi (requests) ✓, hex (phoenix) ✓ — backend picks GitHub Releases rather than HexDocs, crates (serde) ✓, vcpkg (fmt) ✓, maven (com.google.guava:guava) ✓, packagist (symfony/console) ✓, nuget (Newtonsoft.Json) returns generic BACKEND_ERROR, zig (zig-clap) returns Package not found (indexing gap).
  • --no-body payload size: npm:typescript --limit 20 → 17.3 KB with bodies, 3.4 KB without = 5.13× smaller.

Test plan

  • bun test passes (1062 / 0)
  • bun run typecheck clean
  • bun run build clean
  • bun run lint clean (no new warnings)
  • Live-smoke against pkgseer: latest, range, repo-URL, --no-body, payload size delta, tag-style rejection, @spec@version rejection, body-preview cap + footer, --verbose uncap
  • Registry-behaviour matrix (9 registries)

🤖 Generated with Claude Code

@jlitola
jlitola force-pushed the feat/pkg-intel-changelog branch 2 times, most recently from 63ca06a to 8118922 Compare April 21, 2026 05:37
Fourth Wave 1 package-intelligence tool, following `package_summary`
(#13), `package_vulnerabilities` (#14), `package_dependencies` (#16
+ follow-up #17). Behind the existing `code_navigation` capability
gate. Both surfaces share a single request builder, envelope builder,
and error classifier; a parity test asserts `toEqual` JSON across
the two surfaces for every service-sourced fixture.

## Surfaces

- **`githits pkg changelog [spec]`** — default latest-mode output is
  a summary row (identity · source · mode · entry count) plus, per
  entry, a `version  date  url` header followed by the first 10
  lines of the markdown body (with a `… (+N more lines — use
  --verbose for the full body)` footer when the body is longer).
  `--verbose` uncaps the body preview; `--no-body` drops bodies
  entirely from both terminal and `--json`. `--from <v>` switches
  to range mode (all entries between `--from` and `--to`/latest;
  `--limit` rejected). `--to <v>` / `--limit <n>` shape latest
  mode. `--git-ref` targets a branch/tag for CHANGELOG.md source.
  `--repo-url <url>` is an alternative addressing mode, mutually
  exclusive with `<spec>`.
- **`package_changelog`** — MCP tool with the same envelope. Dual
  addressing: `registry` + `package_name` XOR `repo_url`.
  Permissive Zod schema + in-handler validation. `include_bodies`
  (default true) mirrors CLI's `--no-body` and controls JSON
  envelope bodies.

## Design choices

- **Dual addressing is unique to this tool.** P1 / P2 / P3 all
  accept only `registry` + `package_name` because their underlying
  backend queries are registry-metadata APIs. `packageChangelog`
  is intrinsically repo-level (sources: GitHub Releases,
  CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode
  in the GraphQL signature. Exposing it on MCP was non-negotiable:
  `packageSummary` cannot resolve repo-URL → spec, so agents
  starting from a repo URL had no path in. Documented in
  `tools.md` so future tool authors don't cargo-cult the
  asymmetry.
- **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat
  `@version` as "for this exact version"; changelog has no
  single-version query. Silently remapping to `toVersion` would be
  a client-invented semantic shift. Rejected with
  `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`.
- **Body preview capped at 10 lines by default.** Release bodies
  routinely run 50-100+ lines — showing them unbounded would
  swamp the terminal; showing none would make the whole command
  useless for answering "what changed". 10-line cap shows the
  first one or two sections plus preamble, which is usually
  enough to answer the question; `--verbose` lifts the cap when
  full context is needed; `--no-body` drops bodies entirely for
  a pure timeline view. JSON envelope is always full-body unless
  `--no-body` / `include_bodies: false` is set — `--verbose` is
  terminal-only.
- **Data-first envelope.** `{registry|repoUrl, source, mode,
  entries: {count, items}, filter?}`. `source` always present
  here (null-source case promoted to `NOT_FOUND` at the service
  boundary; envelope builder has a defence-in-depth throw if the
  invariant is ever violated upstream). `mode` derived from
  request — `"range"` iff `fromVersion` non-null, `"latest"`
  otherwise. `entries.count` computed client-side from
  `items.length`; backend count not selected on the wire.
- **`version` kept when null, other per-entry nullables stripped.**
  `version` is the primary key agents index by. Empty-string
  values (e.g. `body: ""`) are preserved — distinct from absent
  — so agents can tell "empty release notes" from "no notes
  field". Terminal formatter renders `(empty release notes)`
  sentinel for empty-string bodies.
- **`filter.*` tracks explicit fields only.** Builder returns an
  `explicitFilterFields` set; envelope emits `filter.*` only for
  caller-supplied inputs. Backend defaults (`limit: 10`,
  `toVersion: latest`) never round-trip as caller intent.
- **`include_bodies` lever.** Default true. `false` drops `body`
  from every item explicitly; other fields preserved so agents
  still get the version / date / URL timeline. Measured 5.13×
  envelope size reduction on a 20-entry `npm:typescript` request
  (17.3 KB → 3.4 KB).
- **`--no-body` + `--verbose` is an error.** Contradictory
  intents; CLI rejects with an actionable hint rather than
  silently ignoring `--verbose`.
- **MCP `limit` validation in the shared builder, not Zod.** If
  Zod hard-rejected `limit: 51` at the SDK level, the agent would
  see a raw SDK error instead of our shared
  `{error, code, retryable}` envelope. Schema is permissive;
  builder enforces bounds.
- **Registry coverage — all 9 on the wire, no client-side
  restriction.** `packageChangelog` is source-pull, not
  registry-query. Live-smoke confirmed npm / pypi / hex / crates
  / vcpkg / maven / packagist return useful data; nuget returns a
  generic `BACKEND_ERROR`; zig packages weren't in the backend
  index. All paths produce the shared envelope correctly.
- **Mode mutex enforced client-side.** `--from` / `from_version`
  + `--limit` / `limit` → `INVALID_ARGUMENT` with actionable hint.
- **`metadata` dropped in v1.** Source-specific opaque
  `GenericJSON`; revisit via agent feedback.
- **Shared `promoteGenericVersionNotFound` extended.** Now
  recognises `fromVersion` / `toVersion` in addition to
  `version`. `registry` / `packageName` made optional so
  repo-URL-addressed requests flow through. Preference order
  `version → fromVersion → toVersion`. P2 / P3 regressions
  guarded by existing tests + 5 new helper tests.

## Tests

- `bun test` — 1062 pass, 0 fail.
- `bun run typecheck` / `bun run build` / `bun run lint` — clean.
- Request builder, envelope builder, terminal formatter, MCP tool,
  CLI action, parity fixtures (12) — see docs for full matrix.
- Live-smoke against production pkgseer: latest / range /
  repo-URL / `--no-body` / error paths / registry matrix /
  body-preview cap / `--verbose` uncap / truncation footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jlitola
jlitola force-pushed the feat/pkg-intel-changelog branch from 8118922 to 845bddf Compare April 21, 2026 06:22
@jlitola
jlitola merged commit 8219226 into main Apr 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant